package com.wh1200.mybatis.xmlless.resolver

import com.wh1200.mybatis.xmlless.model.*
import com.wh1200.mybatis.xmlless.util.BuilderAssistantHolder
import com.wh1200.mybatis.xmlless.util.ModelUtil
import org.apache.ibatis.builder.ResultMapResolver
import org.apache.ibatis.mapping.ResultFlag
import org.apache.ibatis.mapping.ResultMapping
import org.apache.ibatis.type.JdbcType
import org.apache.ibatis.type.StringTypeHandler

/**
 *
 * Created by 吴昊 on 2018-12-15.
 *
 * @author 吴昊
 * @since 0.0.8
 */
object ResultMapResolver {

  fun resolveResultMap(
      id: String,
      returnType: Class<*>,
      query: Query? = null,
      optionalProperties: Properties = Properties(),
      isJsonResult: Boolean = false
  ): String? {
    val builderAssistant = BuilderAssistantHolder.get()
    if (returnType.name.startsWith("java.lang") || returnType.isEnum) {
      return null
    }
    val copyId = id.replace(".", "_")
    if (builderAssistant.configuration.hasResultMap(copyId)) {
      return copyId
    }
    val mappings = MappingResolver.getMappingCache(returnType)
    val isJsonType = isJsonResult || isJsonType(returnType, query)
    val resultMap = ResultMapResolver(
        builderAssistant, copyId,
        if (isJsonType) {
          JsonWrapper::class.java
        } else {
          returnType
        }, null, null,
        resolveResultMappings(
            mappings, query,
            optionalProperties, returnType, copyId
        ), true
    ).resolve()
    if (!builderAssistant.configuration.hasResultMap(resultMap.id)) {
      builderAssistant.configuration.addResultMap(resultMap)
    }
    return copyId
  }

  private fun isJsonType(modelClass: Class<*>, query: Query?): Boolean {
    return ModelUtil.isJsonMappingClass(modelClass) && query != null && query.properties.includes.size == 1
  }

  private fun buildByMapping(
      id: String, mapping: FieldMapping,
      tableInfo: XmllessTableInfo, modelClass: Class<*>
  ): ResultMapping? {
    if (mapping.selectIgnore) {
      return null
    }
    val builderAssistant = BuilderAssistantHolder.get()
    val builder = ResultMapping.Builder(
        builderAssistant.configuration,
        mapping.property
    )
    if (mapping.property == tableInfo.keyProperty) {
      builder.flags(listOf(ResultFlag.ID))
    }
    if (mapping.joinInfo == null) {
      builder.javaType(mapping.tableFieldInfo.propertyType)
      builder.column(mapping.property)
    } else {
      processJoinProperty(mapping, builder, id, modelClass)
    }
    builder.typeHandler(TypeResolver.resolveTypeHandler(mapping))
    return builder.build()
  }

  private fun processJoinProperty(
      mapping: FieldMapping,
      builder: ResultMapping.Builder,
      id: String,
      modelClass: Class<*>
  ) {
    when (val joinInfo = mapping.joinInfo) {
      is PropertyJoinInfo -> {
        val rawType = joinInfo.typeInfo.rawType
        if (Collection::class.java.isAssignableFrom(rawType)) {
          builder.column(null)
          builder.javaType(rawType)
          builder.nestedResultMapId(
              createSinglePropertyResultMap(
                  id + "_" + mapping.property, joinInfo.typeInfo.realType.getJavaClass(),
                  joinInfo.propertyColumn.alias
              )
          )
        } else {
          builder.javaType(joinInfo.typeInfo.realType.getJavaClass())
          builder.column(joinInfo.propertyColumn.alias)
        }
      }

      is ObjectJoinInfo   -> {
        if (joinInfo.associationPrefix.isNotBlank()) {
          builder.columnPrefix(joinInfo.associationPrefix)
        }
        builder.javaType(joinInfo.typeInfo.rawType)
        val mappedType = joinInfo.typeInfo.realType.getJavaClass()
        builder.nestedResultMapId(
            when (mappedType) {
              modelClass -> id
              else       -> resolveResultMap(
                  id + "_" + mapping.property, mappedType,
                  null, joinInfo.selectProperties
              )
            }
        )
      }
    }
  }

  private fun createSinglePropertyResultMap(
      id: String,
      realType: Class<*>,
      column: String
  ): String {
    val builderAssistant = BuilderAssistantHolder.get()
    val copyId = id.replace(".", "_")
    if (builderAssistant.configuration.hasResultMap(copyId)) {
      return copyId
    }
    val resultMap = ResultMapResolver(
        builderAssistant, copyId,
        realType, null, null,
        listOf(
            ResultMapping.Builder(
                builderAssistant.configuration,
                null, column, Object::class.java
            ).build()
        ), true
    ).resolve()

    if (!builderAssistant.configuration.hasResultMap(resultMap.id)) {
      builderAssistant.configuration.addResultMap(resultMap)
    }
    return copyId
  }

  private fun isMappingPropertyInQuery(mapping: FieldMapping, query: Query): Boolean {
    if (query.properties.isIncludeNotEmpty() && mapping.property in query.properties) {
      return true
    }
    if (mapping.joinInfo != null && mapping.joinInfo is ObjectJoinInfo) {
      return query.properties.includes.filter { it.contains('.') && it !in query.properties.excludes }
          .map { it.split('.')[0] }.contains(mapping.property)
    }
    return false
  }

  private fun resolveResultMappings(
      mappings: FieldMappingList?,
      query: Query?,
      optionalProperties: Properties,
      modelClass: Class<*>,
      copyId: String
  ): List<ResultMapping> {
    if (isJsonType(modelClass, query)) {
      val column = query!!.mappings.mappings
          .find { it.property == query.properties.includes[0] }?.column
        ?: throw IllegalStateException("未找到【${query.properties.includes[0]}】对应的列")
      return listOf(
          ResultMapping.Builder(
              BuilderAssistantHolder.get().configuration,
              null, column, String::class.java
          ).typeHandler(StringTypeHandler())
              .flags(arrayListOf(ResultFlag.CONSTRUCTOR))
              .jdbcType(JdbcType.VARCHAR)
              .build()
      )
    }
    return mappings?.mappings?.mapNotNull { mapping ->
      when {
        query != null && (query.properties.isIncludeNotEmpty()
            && !isMappingPropertyInQuery(mapping, query))      -> null

        (optionalProperties.isIncludeNotEmpty()
            && mapping.property !in optionalProperties)
            || mapping.property in optionalProperties.excludes -> null

        else                                                   -> buildByMapping(
            copyId,
            mapping,
            mappings.tableInfo,
            modelClass
        )
      }
    } ?: listOf()
  }

}
